﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using ProceduralGolf.generation;
using ProceduralGolf.terrain;
using ProceduralGolf.physics;
using JigLibX.Collision;
using JigLibX.Physics;

namespace ProceduralGolf
{
    class GolfCourse
    {
        public BinaryTriangleTerrain btt; // for displaying       
        TerrainObject terrain; // for physics

        public Heightmap Heightmap { get { return heightmap; } }
        public float Scale { get { return scale; } }
        public GolfCourseLayout.GolfCourseLayer[,] Layermap { get { return layermap; } }
        public GolfCourseLayout Layout { get { return layout; } }

        Heightmap heightmap;
        GolfCourseLayout layout;
        GolfCourseLayout.GolfCourseLayer[,] layermap;
        float scale;

        // camera stuff
        ChaseCamera chaseCam;
        AimCam aimCam;
        Camera camera;

        Matrix world = Matrix.CreateTranslation(Vector3.Zero);
        Matrix view;
        Matrix projection;
        Vector3 viewVector;

        GolfBall ball;

        PowerBar powerBar;

        float maxSpeed = 1.0f;
        float speedTimeout = 500.0f;
        float speedTimer = 0;

#if !XBOX
        KeyboardState prevKeyboardState = Keyboard.GetState();
#endif
        GamePadState prevGamepadState = GamePad.GetState(PlayerIndex.One);
        bool prevBallActive = true;

        GraphicsDevice graphicsDevice;
        float timer;

        Flag flag;

        ClubManager clubManager;

        MiniMap miniMap;

        SpriteFont font;
        SpriteBatch spriteBatch;

        GrassGenerator grassGenerator;
        Grass[] grass;

        // collision stuff
        List<Body> bodyList; // add all bodies, which should be checked to this list
        List<CollisionInfo> collisionInfos; // this is filled by collisionfuntor
        CollisionFunctor collisionFunctor; // as parameter pass CollisionInfo-List

        private int hole;
        public int Hole { get { return hole; } }

        private int shotCtr = 0;
        public int Shots { get { return shotCtr; } }

        bool won = false;
        public bool Won { get { return won; } }

        public GolfCourse(Game game, Heightmap heightmap, GolfCourseLayout layout, float scale, int hole)
        {
            LoadingScreen loadingScreen = LoadingScreen.GetLoadingScreen(game);

            this.graphicsDevice = game.GraphicsDevice;
            this.heightmap = heightmap;
            this.layout = layout;
            this.layermap = layout.getGolfCourseLayout();
            this.scale = scale;
            this.hole = hole;

            loadingScreen.Message = "generating BTT";

            btt = new BinaryTriangleTerrain(game, this);

            loadingScreen.Message = "generating BTT buffers";

            float maxVariance = Configuration.Config.SettingGroups["Terrain"].Settings["MaxVariance"].GetValueAsFloat();
            btt.Generate(game.GraphicsDevice, maxVariance, Vector3.Zero);
            this.heightmap = btt.Heightmap;

            loadingScreen.Message = "generating terrainObject";

            terrain = new TerrainObject(game, btt);

            loadingScreen.Message = "generating golfball";

            ball = new GolfBall(game, Vector3.Zero);
            
            PlaceBall(layout.Tee*scale);


            chaseCam = new ChaseCamera();
            aimCam = new AimCam();

            camera = aimCam;
            UpdateMatrices();

            loadingScreen.Message = "generating powerbar";

            powerBar = new PowerBar(game);
            powerBar.generatePowerBar(32);
            powerBar.Position = new Vector2(game.GraphicsDevice.Viewport.Width/2, game.GraphicsDevice.Viewport.Height*0.85f);

            clubManager = new ClubManager(game);
            clubManager.Position = powerBar.Position;

            miniMap = new MiniMap(game, layout);
            miniMap.Position = new Vector2(game.GraphicsDevice.Viewport.Width - miniMap.Width, game.GraphicsDevice.Viewport.Height - miniMap.Height);

            float height = heightmap.getHeight((int)layout.Hole.X, (int)layout.Hole.Y) * scale;
            flag = new Flag(game, new Vector3(layout.Hole.X * scale, height, layout.Hole.Y * scale));

            bodyList = new List<Body>(); // add all bodies, which should be checked to this list
            bodyList.Add(ball.Body);
            collisionInfos = new List<CollisionInfo>(); // this is filled by collisionfuntor
            collisionFunctor = new BasicCollisionFunctor(collisionInfos); // as parameter pass CollisionInfo-List

            ball.Body.SetActivityThreshold(0.75f, 90f);
            ball.Body.SetDeactivationTime(0.1f);

            ball.addCallBack(new CollisionCallbackFn(HandleHoleCollision));

            font = game.Content.Load<SpriteFont>("MyFont");
            spriteBatch = new SpriteBatch(game.GraphicsDevice);

            loadingScreen.Message = "generating grass";

            grassGenerator = new GrassGenerator(game);
            grass = grassGenerator.getGrass(this);
        }

        ~GolfCourse()
        {
        }

        private void PlaceBall(Vector2 position)
        {
            int x = (int)(position.X / scale);
            int y = (int)(position.Y / scale);

            float height = heightmap.getHeight(x, y) * scale + ball.Radius;

            ball.Body.MoveTo(new Vector3(position.X, height, position.Y), ball.Body.Orientation);
            ball.Body.ClearForces();
            ball.Body.SetInactive();
        }

        bool BallStopped()
        {
            return prevBallActive && !ball.Body.IsActive;
        }

#if !XBOX
        public void Update(KeyboardState keyboardState, GameTime gameTime)
        {
            bool ballActive = ball.Body.IsActive;
            miniMap.SetBallPosition(won ? layout.Hole : new Vector2(ball.Body.Position.X, ball.Body.Position.Z) / scale);
            if (ballActive)
            {
                // we assume chasecam is already set as the active camera
                if (!won)
                {
                    chaseCam.ChasePosition = ball.Body.Position;
                    Vector3 cameraDirection = chaseCam.ChaseDirection;
                    cameraDirection.Y = 0.0f;
                    cameraDirection.Normalize();
                    float max = 50.0f;
                    float y = (ball.Body.Position.Y - heightmap.getHeight((int)(ball.Body.Position.X / scale), (int)(ball.Body.Position.Z / scale)) * scale) / max;
                    if (y > 1.0f) y = 1.0f;
                    cameraDirection.Y = -y;
                    cameraDirection.Normalize();
                    chaseCam.ChaseDirection = cameraDirection;
                }

                chaseCam.Update(keyboardState);

                PhysicsSystem.CurrentPhysicsSystem.CollisionSystem.DetectAllCollisions(bodyList, collisionFunctor, null, 0.5f);
                if (collisionInfos.Count > 0) // check if there are new collisionInfos
                {
                    foreach (CollisionInfo info in collisionInfos)
                    {
                        CollisionSkin skin0 = info.SkinInfo.Skin0;
                        CollisionSkin skin1 = info.SkinInfo.Skin1;
                        if (skin0.Equals(ball.Skin))
                        {
                            Vector3 collisionPoint = skin0.Owner.Position + info.PointInfo[0].R0;
                            HandleCollisionEvent(skin1, collisionPoint);
                            break;
                        }
                    }
                    // clear list, else you cannot decide which CollisionInfos are new or old
                    collisionInfos.Clear();
                }

            }
            else
            {
                if (BallStopped())
                {
                    if (won)
                    { }
                    else
                    {
                        shotCtr += 1;
                        camera = aimCam;
                        aimCam.Position = ball.Body.Position;
                        aimCam.SetTarget(new Vector3(layout.Hole.X * scale, aimCam.Position.Y, layout.Hole.Y * scale));
                        timer = 0;
                    }
                }
                // aim
                aimCam.Update(keyboardState);
                if (keyboardState.IsKeyDown(Keys.Up) && prevKeyboardState.IsKeyUp(Keys.Up))
                {
                    clubManager.nextClub();
                }
                else if (keyboardState.IsKeyDown(Keys.Down) && prevKeyboardState.IsKeyUp(Keys.Down))
                {
                    clubManager.prevClub();
                }
                else if (keyboardState.IsKeyDown(Keys.S))
                {
                    if (prevKeyboardState.IsKeyUp(Keys.S))
                    {
                        // reset timer etc
                        //timer = 0;
                    }
                    else
                    {
                        // increment timer
                        timer += (float)gameTime.ElapsedGameTime.TotalMilliseconds;
                    }

                }
                else if (prevKeyboardState.IsKeyDown(Keys.S))
                {
                    Vector2 ballPosition = toLayoutCoordinates(ball.Body.Position);
                    // hit ball
                    Vector3 direction = Vector3.TransformNormal(-Vector3.UnitZ, Matrix.CreateFromYawPitchRoll(-aimCam.angle, MathHelper.PiOver4, 0.0f));
                    ball.ApplyForce(clubManager.hit(-aimCam.angle, powerBar.Power, layermap[(int)ballPosition.X, (int)ballPosition.Y], shotCtr == 1));
                    camera = chaseCam;
                    //Vector3 cameraDirection = new Vector3(layout.Hole.X * scale - ball.Body.Position.X, 0.0f, layout.Hole.Y * scale - ball.Body.Position.Z);
                    Vector3 cameraDirection = new Vector3(ball.Body.Velocity.X, 0.0f, ball.Body.Velocity.Z);
                    cameraDirection.Normalize();
                    chaseCam.ChaseDirection = cameraDirection;
                    //Vector3 cameraDirection = new Vector3(direction.X, 0.0f, direction.Z);
                    //cameraDirection.Normalize();
                    //chaseCam.ChaseDirection = cameraDirection;
                    chaseCam.ChasePosition = ball.Body.Position;
                    chaseCam.Reset();
                }

            }
            powerBar.calculatePower(timer);
            prevKeyboardState = keyboardState;
            prevBallActive = ballActive;
            UpdateMatrices();
        }

#endif

        public void Update(GamePadState gamepadState, GameTime gameTime)
        {
            bool ballActive = ball.Body.IsActive;
            miniMap.SetBallPosition(won ? layout.Hole : new Vector2(ball.Body.Position.X, ball.Body.Position.Z) / scale);
            if (ballActive)
            {
                // we assume chasecam is already set as the active camera
                if (!won)
                {
                    chaseCam.ChasePosition = ball.Body.Position;
                    Vector3 cameraDirection = chaseCam.ChaseDirection;
                    cameraDirection.Y = 0.0f;
                    cameraDirection.Normalize();
                    float max = 50.0f;
                    float y = (ball.Body.Position.Y - heightmap.getHeight((int)(ball.Body.Position.X / scale), (int)(ball.Body.Position.Z / scale)) * scale) / max;
                    if (y > 1.0f) y = 1.0f;
                    cameraDirection.Y = -y;
                    cameraDirection.Normalize();
                    chaseCam.ChaseDirection = cameraDirection;
                }

                chaseCam.Update(gamepadState);

                PhysicsSystem.CurrentPhysicsSystem.CollisionSystem.DetectAllCollisions(bodyList, collisionFunctor, null, 0.5f);
                if (collisionInfos.Count > 0) // check if there are new collisionInfos
                {
                    foreach (CollisionInfo info in collisionInfos)
                    {
                        CollisionSkin skin0 = info.SkinInfo.Skin0;
                        CollisionSkin skin1 = info.SkinInfo.Skin1;
                        if (skin0.Equals(ball.Skin))
                        {
                            Vector3 collisionPoint = skin0.Owner.Position + info.PointInfo[0].R0;
                            HandleCollisionEvent(skin1, collisionPoint);
                            break;
                        }
                    }
                    // clear list, else you cannot decide which CollisionInfos are new or old
                    collisionInfos.Clear();
                }

                if (ball.Body.Velocity.Length() < maxSpeed)
                {
                    speedTimer += (float)gameTime.ElapsedGameTime.TotalMilliseconds;
                }
                else
                {
                    speedTimer = 0;
                }
                if (speedTimer > speedTimeout)
                {
                    PlaceBall(new Vector2(ball.Body.Position.X, ball.Body.Position.Z));
                }

            }
            else
            {
                if (BallStopped())
                {
                    if (won)
                    { }
                    else
                    {
                        shotCtr += 1;
                        camera = aimCam;
                        aimCam.Position = ball.Body.Position;
                        aimCam.SetTarget(new Vector3(layout.Hole.X * scale, aimCam.Position.Y, layout.Hole.Y * scale));
                        timer = 0;
                    }
                }
                // aim
                aimCam.Update(gamepadState);
                if (gamepadState.IsButtonDown(Buttons.RightShoulder) && prevGamepadState.IsButtonUp(Buttons.RightShoulder))
                {
                    clubManager.nextClub();
                }
                else if (gamepadState.IsButtonDown(Buttons.LeftShoulder) && prevGamepadState.IsButtonUp(Buttons.LeftShoulder))
                {
                    clubManager.prevClub();
                }
                else if (gamepadState.IsButtonDown(Buttons.A))
                {
                    if (prevGamepadState.IsButtonUp(Buttons.A))
                    {
                        // reset timer etc
                        //timer = 0;
                    }
                    else
                    {
                        // increment timer
                        timer += (float)gameTime.ElapsedGameTime.TotalMilliseconds;
                    }

                }
                else if (prevGamepadState.IsButtonDown(Buttons.A))
                {
                    Vector2 ballPosition = toLayoutCoordinates(ball.Body.Position);
                    // hit ball
                    Vector3 direction = Vector3.TransformNormal(-Vector3.UnitZ, Matrix.CreateFromYawPitchRoll(-aimCam.angle, MathHelper.PiOver4, 0.0f));
                    ball.ApplyForce(clubManager.hit(-aimCam.angle, powerBar.Power, layermap[(int)ballPosition.X, (int)ballPosition.Y], shotCtr == 1));
                    camera = chaseCam;
                    //Vector3 cameraDirection = new Vector3(layout.Hole.X * scale - ball.Body.Position.X, 0.0f, layout.Hole.Y * scale - ball.Body.Position.Z);
                    Vector3 cameraDirection = new Vector3(ball.Body.Velocity.X, 0.0f, ball.Body.Velocity.Z);
                    cameraDirection.Normalize();
                    chaseCam.ChaseDirection = cameraDirection;
                    //Vector3 cameraDirection = new Vector3(direction.X, 0.0f, direction.Z);
                    //cameraDirection.Normalize();
                    //chaseCam.ChaseDirection = cameraDirection;
                    chaseCam.ChasePosition = ball.Body.Position;
                    chaseCam.Reset();
                }

            }
            powerBar.calculatePower(timer);
            prevGamepadState = gamepadState;
            prevBallActive = ballActive;
            UpdateMatrices();
        }

        void UpdateMatrices()
        {
            view = camera.GetViewMatrix();
            projection = camera.GetProjectionMatrix();
            viewVector = camera.GetViewVector();
        }


        public bool HandleHoleCollision(CollisionSkin owner, CollisionSkin collidee)
        {
            // here is handled what happens if your Object collides with another special Object (= OtherObject)
            if (collidee.Equals(terrain.Skin))
            {
                if (won)
                {
                    return false;
                }
                float maxVelocity = 5.0f;
                float holeRadius = 0.5f;
                Vector2 position = toLayoutCoordinates(owner.Owner.Position);
                if (Vector2.Distance(position, layout.Hole) < holeRadius &&
                    new Vector2(ball.Body.Velocity.X, ball.Body.Velocity.Z).Length() < maxVelocity)
                {
                    won = true;
                    ball.Body.Velocity = Vector3.Zero;
                    ball.Body.AngularVelocity = Vector3.Zero;
                    return false;
                }
            }
            // all other collisions will be handled by physicengine
            return true;
        }

        public void HandleCollisionEvent(CollisionSkin skin, Vector3 position)
        {
            if (skin.Equals(terrain.Skin))
            {
                Vector2 layoutPos = toLayoutCoordinates(position);
                //float holeRadius = 0.5f;
                //if ((Vector2.Distance(layoutPos, layout.Hole) < holeRadius) &&
                //   (new Vector2(ball.Body.Velocity.X, ball.Body.Velocity.Z).Length() < 8.0f))
                //{
                //    Console.WriteLine("You Won");    
                //    PlaceBall(layout.Hole*scale);
                //    won = true;
                //}


                GolfCourseLayout.GolfCourseLayer layer = layermap[(int)layoutPos.X, (int)layoutPos.Y];

                //Console.WriteLine("collision with " + layer.ToString());

                float velocityFactor = 1.0f;
                float angularVelocityFactor = 1.0f;
                switch (layer)
                {
                    case GolfCourseLayout.GolfCourseLayer.FAIRWAY:
                        angularVelocityFactor = 0.9f;
                        break;
                    case GolfCourseLayout.GolfCourseLayer.FRINGE:
                        angularVelocityFactor = 0.75f;
                        break;
                    case GolfCourseLayout.GolfCourseLayer.GREEN:
                        break;
                    case GolfCourseLayout.GolfCourseLayer.HAZARDS:
                        velocityFactor = 0.75f;
                        angularVelocityFactor = 0.25f;
                        break;
                    case GolfCourseLayout.GolfCourseLayer.ROUGH:
                        velocityFactor = 0.9f;
                        angularVelocityFactor = 0.5f;
                        break;
                    default:
                        break;
                }
                ball.Body.AngularVelocity *= angularVelocityFactor;
                ball.Body.Velocity *= velocityFactor;
            }
            else if (skin.Equals(flag.Skin))
            {
            }

        }

        public Vector2 toLayoutCoordinates(Vector3 worldPos)
        {
            return new Vector2(worldPos.X, worldPos.Z) / scale;
        }

        public void Draw()
        {
            Draw(world, view, projection, viewVector);
        }

        public void Draw(Matrix world, Matrix view, Matrix projection, Vector3 viewVector)
        {
            graphicsDevice.RenderState.DepthBufferEnable = true;
            graphicsDevice.RenderState.AlphaBlendEnable = false;
            graphicsDevice.RenderState.AlphaTestEnable = false;

            btt.Draw(world, view, projection, viewVector);
            flag.Draw(world, view, projection, Color.LightGray);
            ball.Draw(world, view, projection, Color.White);

            graphicsDevice.RenderState.AlphaBlendEnable = true;
            graphicsDevice.RenderState.AlphaTestEnable = true;
            foreach (Grass g in grass)
            {
                g.Draw(world, view, projection, camera.GetPosition());
            }

            powerBar.Draw();
            clubManager.Draw();
            miniMap.Draw();
            DrawCourseInfo();
            
        }

        public void reset()
        {
            PlaceBall(layout.Tee * scale);
            prevBallActive = true;
            shotCtr = 0;
            won = false;
        }

        public void freeze()
        {
            ball.Body.SetInactive();
        }

        //public void DrawCourseInfo(Vector2 position)
        //{
        //    String[] msgs = {
        //        "Hole " + Hole,
        //        "Course length " + (layout.Hole - layout.Tee).Length().ToString() + " yd",
        //        "Par " + layout.Par,
        //        "Shot " + shotCtr.ToString(),
        //        "Remaining Distance " + (layout.Hole - toLayoutCoordinates(ball.Body.Position)).Length().ToString() + " yd" 
        //    };

        //    Vector2 sep = new Vector2(0, font.LineSpacing);
      
        //    spriteBatch.Begin();
        //    foreach(String msg in msgs)
        //    {
        //        spriteBatch.DrawString(font, msg, position, Color.White);
        //        position += sep;
        //    }
        //    spriteBatch.End();
        //}

        public void DrawCourseInfo()
        {
            String info0 = String.Format("Hole {0} | Par {1} | {2:F1}yd", hole, layout.Par, Vector2.Distance(layout.Hole, layout.Tee));
            String info1 = String.Format("Shot {0} | {1:F1}yd remaining", shotCtr, won ? 0.0f : Vector2.Distance(layout.Hole, toLayoutCoordinates(ball.Body.Position)));

            float offset = 10.0f;
            Vector2 top = new Vector2(graphicsDevice.Viewport.Width/2, offset);
            Vector2 bottom = new Vector2(graphicsDevice.Viewport.Width/2, graphicsDevice.Viewport.Height - offset);
            spriteBatch.Begin();
            DrawString(info0, top, Color.White);
            DrawString(info1, bottom, Color.White);
            spriteBatch.End();
        }

        // assume spritebatch.begin has been called
        void DrawString(String msg, Vector2 position, Color color)
        {
            Vector2 size = font.MeasureString(msg);
            spriteBatch.DrawString(font, msg, position - size / 2, color);
        }

    }
}
